﻿using System;
using System.Collections.Generic;
using System.Configuration.Provider;
using System.Data.SqlClient;
using System.Web.Hosting;

namespace Altairis.IdentityToolkit.PersonalCard {
    /// <summary>
    /// Class representing the SQL implementation of <see cref="PersonalCardIdentityLinkProvider"/>.
    /// </summary>
    public class SqlPersonalCardIdentityLinkProvider : PersonalCardIdentityLinkProvider {
        private const string DefaultName = "SqlPersonalCardIdentityLinkProvider";
        private const string DefaultDescription = "SQL IdentityLink provider for Personal Information Cards";
        private const string DefaultTableName = "PersonalCardLink";
        private const string DefaultUserNameColumn = "UserName";
        private const string DefaultPPIDColumn = "PPID";
        private const string DefaultSSIDColumn = "SSID";
        private const string DefaultDateAssignedColumn = "DateAssigned";

        /// <summary>
        /// Gets or sets the name of the connection string.
        /// </summary>
        /// <value>The name of the connection string.</value>
        public string ConnectionStringName { get; private set; }

        /// <summary>
        /// Gets or sets the connection string.
        /// </summary>
        /// <value>The connection string.</value>
        public string ConnectionString { get; private set; }

        /// <summary>
        /// Gets or sets the name of the table where identity links are stored.
        /// </summary>
        /// <value>The name of the table.</value>
        public string TableName { get; private set; }

        /// <summary>
        /// Gets or sets the name of the user column.
        /// </summary>
        /// <value>The name of the user column.</value>
        public string UserNameColumn { get; private set; }

        /// <summary>
        /// Gets or sets the PPID column name.
        /// </summary>
        /// <value>The PPID column name.</value>
        public string PPIDColumn { get; private set; }

        /// <summary>
        /// Gets or sets the SSID column name.
        /// </summary>
        /// <value>The SSID column name.</value>
        public string SSIDColumn { get; private set; }

        /// <summary>
        /// Gets or sets the link date assigned column name.
        /// </summary>
        /// <value>The link date assigned column name.</value>
        public string DateAssignedColumn { get; private set; }

        /// <summary>
        /// Initializes the provider.
        /// </summary>
        /// <param name="name">The friendly name of the provider.</param>
        /// <param name="config">A collection of the name/value pairs representing the provider-specific attributes specified in the configuration for this provider.</param>
        /// <exception cref="T:System.ArgumentNullException">The name of the provider is null.</exception>
        /// <exception cref="T:System.ArgumentException">The name of the provider has a length of zero.</exception>
        /// <exception cref="T:System.InvalidOperationException">An attempt is made to call <see cref="M:System.Configuration.Provider.ProviderBase.Initialize(System.String,System.Collections.Specialized.NameValueCollection)"/> on a provider after the provider has already been initialized.</exception>
        public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config) {
            // Initialize base class
            base.Initialize(name, config);

            // Validate arguments
            if (string.IsNullOrEmpty(name)) name = DefaultName;
            if (string.IsNullOrEmpty(config["description"])) config["description"] = DefaultDescription;

            // Get connection string
            this.ConnectionStringName = config["connectionStringName"];
            System.Configuration.ConnectionStringSettings ConnectionStringSettings = System.Configuration.ConfigurationManager.ConnectionStrings[this.ConnectionStringName];
            if (ConnectionStringSettings == null || ConnectionStringSettings.ConnectionString.Trim() == "") throw new ProviderException("Connection string cannot be blank.");
            this.ConnectionString = ConnectionStringSettings.ConnectionString;

            // Get DB object names
            this.TableName = config["tableName"] ?? DefaultTableName;
            this.UserNameColumn = config["userNameColumn"] ?? DefaultUserNameColumn;
            this.PPIDColumn = config["ppidColumn"] ?? DefaultPPIDColumn;
            this.SSIDColumn = config["ssidColumn"] ?? DefaultSSIDColumn;
            this.DateAssignedColumn = config["dateAssignedColumn"] ?? DefaultDateAssignedColumn;
        }

        /// <summary>
        /// Internal method for resolving links between PPID and user names.
        /// </summary>
        /// <param name="ppid">The private personal identifier.</param>
        /// <returns>
        /// user name or <c>null</c> when no link was found.
        /// </returns>
        protected override string InternalResolveLink(string ppid) {
            try {
                using (HostingEnvironment.Impersonate())
                using (var db = this.OpenDatabase())
                using (var cmd = new SqlCommand("SELECT $User FROM $Table WHERE $PPID=@PPID", db)) {
                    cmd.Parameters.AddWithValue("@PPID", ppid);
                    cmd.CommandText = this.ExpandSqlCommandText(cmd.CommandText);
                    return cmd.ExecuteScalar() as string;
                }
            }
            catch { throw; }
        }

        /// <summary>
        /// Internal method for creating links between PPID and user names.
        /// </summary>
        /// <param name="ppid">The PPID.</param>
        /// <param name="userName">The user name.</param>
        protected override void InternalCreateLink(string ppid, string userName) {
            try {
                using (HostingEnvironment.Impersonate())
                using (var db = this.OpenDatabase())
                using (var cmd = new SqlCommand("SELECT COUNT(*) FROM $Table WHERE $PPID=@PPID", db)) {
                    cmd.CommandText = this.ExpandSqlCommandText(cmd.CommandText);
                    cmd.Parameters.AddWithValue("@PPID", ppid);
                    if (Convert.ToInt32(cmd.ExecuteScalar()) > 0) throw new ArgumentOutOfRangeException("ppid", ppid, "This PPID is already attached to some user.");
                    cmd.CommandText = this.ExpandSqlCommandText("INSERT INTO $Table ($User, $PPID, $SSID) VALUES (@User, @PPID, @SSID)");
                    cmd.Parameters.AddWithValue("@User", userName);
                    cmd.Parameters.AddWithValue("@SSID", ComputeSSID(ppid));
                    cmd.ExecuteNonQuery();
                }
            }
            catch { throw; }
        }

        /// <summary>
        /// Removes the link between membership user and PPID.
        /// </summary>
        /// <param name="ppid">The PPID.</param>
        public override void RemoveLink(string ppid) {
            try {
                using (HostingEnvironment.Impersonate())
                using (var db = this.OpenDatabase())
                using (var cmd = new SqlCommand("DELETE FROM $Table WHERE $PPID=@PPID", db)) {
                    cmd.CommandText = this.ExpandSqlCommandText(cmd.CommandText);
                    cmd.Parameters.AddWithValue("@PPID", ppid);
                    if (Convert.ToInt32(cmd.ExecuteNonQuery()) == 0) throw new ArgumentOutOfRangeException("ppid", ppid, "This PPID is not attached to any user.");
                }
            }
            catch { throw; }
        }

        /// <summary>
        /// Removes the link between membership user and PPID.
        /// </summary>
        /// <param name="ppid">The PPID.</param>
        /// <param name="userName">The user name.</param>
        public override void RemoveLink(string ppid, string userName) {
            try {
                using (HostingEnvironment.Impersonate())
                using (var db = this.OpenDatabase())
                using (var cmd = new SqlCommand("DELETE FROM $Table WHERE $PPID=@PPID AND $User=@User", db)) {
                    cmd.CommandText = this.ExpandSqlCommandText(cmd.CommandText);
                    cmd.Parameters.AddWithValue("@PPID", ppid);
                    cmd.Parameters.AddWithValue("@User", userName);
                    if (Convert.ToInt32(cmd.ExecuteNonQuery()) == 0) throw new ArgumentOutOfRangeException("PPID", ppid, "This PPID is not attached to this user.");
                }
            }
            catch { throw; }
        }

        /// <summary>
        /// Gets the identity links for given user.
        /// </summary>
        /// <param name="userName">User name.</param>
        /// <returns>
        /// List of <see cref="PersonalCardIdentityLink"/>s.
        /// </returns>
        public override IEnumerable<PersonalCardIdentityLink> GetLinksForUser(string userName) {
            var retList = new List<PersonalCardIdentityLink>();
            try {
                using (HostingEnvironment.Impersonate())
                using (var db = this.OpenDatabase())
                using (var cmd = new SqlCommand("SELECT $PPID AS PPID, $SSID AS SSID, $Date AS Date FROM $Table WHERE $User=@User ORDER BY $Date", db)) {
                    cmd.CommandText = this.ExpandSqlCommandText(cmd.CommandText);
                    cmd.Parameters.AddWithValue("@User", userName);
                    using (var r = cmd.ExecuteReader()) {
                        while (r.Read()) {
                            retList.Add(new PersonalCardIdentityLink() {
                                UserName = userName,
                                SSID = (string)r["SSID"],
                                PPID = (string)r["PPID"],
                                DateAssigned = (DateTime)r["Date"]
                            });
                        }
                    }
                }
            }
            catch { throw; }
            return retList;
        }

        private SqlConnection OpenDatabase() {
            SqlConnection db = new SqlConnection(this.ConnectionString);
            db.Open();
            return db;
        }

        private string ExpandSqlCommandText(string commandText) {
            if (commandText == null) throw new ArgumentNullException("commandText");
            if (string.IsNullOrEmpty(commandText)) throw new ArgumentException("Value cannot be null or empty string.", "commandText");

            commandText = commandText.Replace("$Table", this.TableName);
            commandText = commandText.Replace("$User", this.UserNameColumn);
            commandText = commandText.Replace("$PPID", this.PPIDColumn);
            commandText = commandText.Replace("$SSID", this.SSIDColumn);
            commandText = commandText.Replace("$Date", this.DateAssignedColumn);
            return commandText;
        }

    }
}
